landsat

This notebook currently relies on HoloViews 1.9 or above. Run conda install -c ioam/label/dev holoviews to install it.

In [1]:
import numpy as np
import xarray as xr
import holoviews as hv
import geoviews as gv
import datashader as ds
import cartopy.crs as ccrs

from holoviews.operation.datashader import regrid, shade
from bokeh.tile_providers import STAMEN_TONER

hv.extension('bokeh', width=80)

Load LandSat Data

LandSat data is measured in different frequency bands, revealing different types of information:

In [2]:
import pandas as pd
band_info = pd.DataFrame([
        (1,  "Aerosol", " 0.43 - 0.45",    0.440,  "30",         "Coastal aerosol"),
        (2,  "Blue",    " 0.45 - 0.51",    0.480,  "30",         "Blue"),
        (3,  "Green",   " 0.53 - 0.59",    0.560,  "30",         "Green"),
        (4,  "Red",     " 0.64 - 0.67",    0.655,  "30",         "Red"),
        (5,  "NIR",     " 0.85 - 0.88",    0.865,  "30",         "Near Infrared (NIR)"),
        (6,  "SWIR1",   " 1.57 - 1.65",    1.610,  "30",         "Shortwave Infrared (SWIR) 1"),
        (7,  "SWIR2",   " 2.11 - 2.29",    2.200,  "30",         "Shortwave Infrared (SWIR) 2"),
        (8,  "Panc",    " 0.50 - 0.68",    0.590,  "15",         "Panchromatic"),
        (9,  "Cirrus",  " 1.36 - 1.38",    1.370,  "30",         "Cirrus"),
        (10, "TIRS1",   "10.60 - 11.19",   10.895, "100 * (30)", "Thermal Infrared (TIRS) 1"),
        (11, "TIRS2",   "11.50 - 12.51",   12.005, "100 * (30)", "Thermal Infrared (TIRS) 2")],
    columns=['Band', 'Name', 'Wavelength Range (µm)', 'Nominal Wavelength (µm)', 'Resolution (m)', 'Description']).set_index(["Band"])
band_info
Out[2]:
Name Wavelength Range (µm) Nominal Wavelength (µm) Resolution (m) Description
Band
1 Aerosol 0.43 - 0.45 0.440 30 Coastal aerosol
2 Blue 0.45 - 0.51 0.480 30 Blue
3 Green 0.53 - 0.59 0.560 30 Green
4 Red 0.64 - 0.67 0.655 30 Red
5 NIR 0.85 - 0.88 0.865 30 Near Infrared (NIR)
6 SWIR1 1.57 - 1.65 1.610 30 Shortwave Infrared (SWIR) 1
7 SWIR2 2.11 - 2.29 2.200 30 Shortwave Infrared (SWIR) 2
8 Panc 0.50 - 0.68 0.590 15 Panchromatic
9 Cirrus 1.36 - 1.38 1.370 30 Cirrus
10 TIRS1 10.60 - 11.19 10.895 100 * (30) Thermal Infrared (TIRS) 1
11 TIRS2 11.50 - 12.51 12.005 100 * (30) Thermal Infrared (TIRS) 2
In [3]:
file_path = '../data/MERCATOR_LC80210392016114LGN00_B%s.TIF'
bands = list(range(1, 12)) + ['QA']
bands = [xr.open_rasterio(file_path%band).load()[0] for band in bands]

Rendering LandSat data as images

The bands measured by LandSat include wavelengths covering the visible spectrum, but also other ranges, and so it's possible to visualize this data in many different ways, in both true color (using the visible spectrum directly) or false color (usually showing other bands). Some examples are shown in the sections below.

Just the Blue Band

Using datashader's default histogram-equalized colormapping, the full range of data is visible in the plot:

In [4]:
%opts RGB [width=600 height=600]
tiles = gv.WMTS(STAMEN_TONER)
tiles * shade(regrid(hv.Image(bands[1])), cmap=['black', 'white']).redim(x='Longitude', y='Latitude')
Out[4]:

You will usually want to zoom in, which will re-rasterize the image if you are in a live notebook, and then re-equalize the colormap to show all the detail available. If you are on a static copy of the notebook, only the original resolution at which the image was rendered will be available, but zooming will still update the map tiles to whatever resolution is requested.

The plots below use a different type of colormap processing, implemented as a custom transfer function:

In [5]:
from datashader.utils import ngjit
nodata= 1

@ngjit
def normalize_data(agg):
    out = np.zeros_like(agg)
    min_val = 0
    max_val = 2**16 - 1
    range_val = max_val - min_val
    col, rows = agg.shape
    c = 40
    th = .125
    for x in range(col):
        for y in range(rows):
            val = agg[x, y]
            norm = (val - min_val) / range_val
            norm = 1 / (1 + np.exp(c * (th - norm))) # bonus
            out[x, y] = norm * 255.0
    return out

def combine_bands(r, g, b):
    xs, ys = r['x'], r['y']
    r, g, b = [ds.utils.orient_array(img) for img in (r, g, b)]
    a = (np.where(np.logical_or(np.isnan(r),r<=nodata),0,255)).astype(np.uint8)    
    r = (normalize_data(r)).astype(np.uint8)
    g = (normalize_data(g)).astype(np.uint8)
    b = (normalize_data(b)).astype(np.uint8)
    col, rows = r.shape
    return hv.RGB((xs, ys[::-1], r, g, b, a), vdims=list('RGBA'))

True Color

Mapping the Red, Green, and Blue bands to the R, G, and B channels of an image reconstructs the image as it would appear to an ordinary camera from that viewpoint:

In [6]:
true_color = combine_bands(bands[3], bands[2], bands[1]).relabel("True Color (R=Red, G=Green, B=Blue)")
tiles * regrid(true_color)
Out[6]:

Again, the raster data will only refresh to a new resolution if you are running a live notebook, because that data is not actually present in the web page; it's held in a separate Python server.

False Color

Other combinations highlight particular features of interest based on the different spectral properties of reflectances from various objects and surfaces, with full data redrawing on zooming if you have a live Python process:

In [7]:
%%opts RGB Curve [width=350 height=350 xaxis=None yaxis=None] {+framewise}

combos = pd.DataFrame([
    (4,3,2,"True color",""),
    (7,6,4,"Urban","False color"),
    (5,4,3,"Vegetation","Color Infrared"),
    (6,5,2,"Agriculture",""),
    (7,6,5,"Penetration","Atmospheric Penetration"),
    (5,6,2,"Healthy Vegetation",""),
    (5,6,4,"Land vs. Water",""),
    (7,5,3,"Atmosphere Removal","Natural With Atmospheric Removal"),
    (7,5,4,"Shortwave Infrared",""),
    (6,5,4,"Vegetation Analysis","")],
    columns=['R', 'G', 'B', 'Name', 'Description']).set_index(["Name"])
combos

def combo(name):
    c=combos.loc[name]
    return regrid(combine_bands(bands[c.R-1],bands[c.G-1],bands[c.B-1])).relabel(name)

(combo("Urban") + combo("Vegetation") + combo("Agriculture") + combo("Land vs. Water")).cols(2)
Out[7]:

All the various ways of combining aggregates supported by xarray are available for these channels, making it simple to make your own custom visualizations highlighting any combination of bands that reveal something of interest.

Revealing the spectrum

The above plots all map some of the measured data into the R,G,B channels of an image, showing all spatial locations but only a restricted set of wavelengths. Alternatively, you could sample across all the measured wavelength bands to show the full spectrum at any given location:

In [8]:
%%opts Curve [width=800 height=300 logx=True]

band_map = hv.HoloMap({i: hv.Image(band) for i, band in enumerate(bands)})

def spectrum(x, y):
    try: 
        spectrum_vals = band_map.sample(x=x, y=y)['z'][:-1]
        point = gv.Points([(x, y)], crs=ccrs.GOOGLE_MERCATOR)
        point = gv.operation.project_points(point, projection=ccrs.PlateCarree())
        label = label = 'Lon: %.3f, Lat: %.3f' % tuple(point.array()[0])
    except:
        spectrum_vals = np.zeros(11)
        label = 'Lon: -, Lat: -'
    
    return hv.Curve((band_info['Nominal Wavelength (µm)'].values, spectrum_vals), label=label,
                    kdims=['Wavelength (µm)'], vdims=['Luminance']).sort()

spectrum(x=-9880000, y=3570000) # Location in Web Mercator coordinates
Out[8]:

We can now combine these two approaches to let you explore the full hyperspectral information at any location in the true-color image, updating the curve whenever you hover over an area of the image:

In [9]:
%%opts Curve RGB [width=450 height=450] Curve [logx=True]

tap = hv.streams.PointerXY(source=true_color)
spectrum_curve = hv.DynamicMap(spectrum, streams=[tap]).redim.range(Luminance=(0, 30000))

tiles * regrid(true_color) + spectrum_curve
Out[9]:

(Of course, just as for the raster data resolution, the plot on the right will update only in a live notebook session, because it needs to run Python code for each mouse pointer position.)

As you can see, even though datashader is not a GIS system, it can be a flexible, high-performance way to explore GIS data when combined with HoloViews, GeoViews, and Bokeh.


Right click to download this notebook from GitHub.